Skip to content

FG Manager - #524

Merged
Tishka17 merged 7 commits into
developfrom
feature/fg
Mar 21, 2026
Merged

Tishka17 merged 7 commits into
developfrom
feature/fg

Conversation

@Tishka17

@Tishka17 Tishka17 commented Mar 20, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Foreground dialog context manager: use "async with" to access dialog state directly and optionally call update without a payload
    • New helper to schedule update processing as a background task
  • Tests

    • Added tests validating foreground dialog behavior across multiple users
  • Documentation

    • New docs and navigation for background/foreground manager usage
  • Other

    • Dialog event name exported at package level

@coderabbitai

coderabbitai Bot commented Mar 20, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 894dad32-f486-40fe-99d7-6415308120bf

📥 Commits

Reviewing files that changed from the base of the PR and between 9daf2b1 and c34da3b.

📒 Files selected for processing (2)
  • src/aiogram_dialog/manager/bg_manager.py
  • src/aiogram_dialog/tools/web_preview.py

📝 Walkthrough

Walkthrough

Adds foreground dialog support: new DialogAction.FG and DialogFgEvent, fg() async-context APIs on manager types (BgManager, ManagerImpl, SubManager, FakeManager), updater support via notify_task, update-handler branch for FG lifecycle, tests, docs, and an example change to use async with manager.fg().

Changes

Cohort / File(s) Summary
Event & Action Types
src/aiogram_dialog/api/entities/update_event.py
Added DialogAction.FG and DialogFgEvent with entered: Future and exited: Future to represent foreground lifecycle.
Manager Protocol
src/aiogram_dialog/api/protocols/manager.py
BaseDialogManager.update accepts `data: dict
Manager Implementations
src/aiogram_dialog/manager/bg_manager.py, src/aiogram_dialog/manager/manager.py, src/aiogram_dialog/manager/sub_manager.py
Relaxed update(...) signatures to accept optional data. Added fg() async context managers: ManagerImpl/SubManager yield self; BgManager.fg() emits DialogFgEvent via updater, awaits entered, yields a DialogManager, and sets exited (propagating exceptions).
Update Handling & Dispatch
src/aiogram_dialog/manager/update_handler.py, src/aiogram_dialog/manager/updater.py
handle_update handles DialogFgEvent by resolving entered with current manager and awaiting exited. Added Updater.notify_task(bot, update) -> Task to schedule processing while preserving context.
Preview & Example
src/aiogram_dialog/tools/preview.py, example/loading.py
FakeManager.update accepts optional data; added FakeManager.fg() async context manager. Example switched background loop to use async with manager.fg() and set m.dialog_data["progress"] inside the foreground context.
Web preview loop
src/aiogram_dialog/tools/web_preview.py
Replaced asyncio.get_event_loop() with asyncio.get_running_loop() in preview/transitions functions.
Tests
tests/test_fg.py
New test module exercising foreground start for another user, with fixtures for two clients and two tests validating cross-user foreground behavior.
Docs & Exports
docs/bg/index.rst, docs/index.rst, src/aiogram_dialog/__init__.py
Added background/foreground documentation page and TOC entry. Exported DIALOG_EVENT_NAME from package __init__.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller (bg task)
    participant BgMgr as BgManager
    participant Updater as Updater
    participant Handler as UpdateHandler
    participant FgMgr as DialogManager

    Caller->>BgMgr: call bg(...).fg() (await)
    BgMgr->>BgMgr: create DialogFgEvent(entered, exited)
    BgMgr->>Updater: notify_task(DialogUpdate(event=FG))
    Updater->>Updater: schedule _process_update (create_task)
    BgMgr->>BgMgr: await event.entered
    Updater->>Handler: _process_update -> handle_update
    Handler->>Handler: detect DialogFgEvent
    Handler->>FgMgr: set event.entered.result(dialog_manager)
    Handler->>Handler: await event.exited
    BgMgr-->>Caller: yield DialogManager (context entered)
    Caller->>FgMgr: perform foreground actions (update/start/...)
    Caller-->>BgMgr: exit context -> BgMgr sets event.exited (None or exception)
    Handler->>Handler: resume after awaited exited
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped from bg to fg with flair,
Futures clasped in midnight air.
fg() opened a bright new gate,
Entered, exited—async fate.
A tiny rabbit cheers the state! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'FG Manager' clearly and concisely describes the main addition: a new foreground (FG) manager feature across multiple files and APIs.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fg

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Tishka17
Tishka17 marked this pull request as ready for review March 21, 2026 11:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_fg.py (1)

33-44: Duplicate handler implementations provide no additional test coverage.

start_for_second_user and start_for_second_user_via_bg are identical. Consider consolidating into a single handler and reusing it across tests, or if the intention is to test different scenarios, make the implementations actually differ.

♻️ Proposed consolidation
 async def start_for_second_user(
         event: Any, dialog_manager: DialogManager,
 ) -> None:
     async with dialog_manager.bg(user_id=2, chat_id=-1).fg() as manager:
         await manager.start(MainSG.start, mode=StartMode.RESET_STACK)
 
-
-async def start_for_second_user_via_bg(
-        event: Any, dialog_manager: DialogManager,
-) -> None:
-    async with dialog_manager.bg(user_id=2, chat_id=-1).fg() as manager:
-        await manager.start(MainSG.start, mode=StartMode.RESET_STACK)

Then reuse start_for_second_user in both tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_fg.py` around lines 33 - 44, Two test handlers are identical:
start_for_second_user and start_for_second_user_via_bg; consolidate by keeping a
single implementation (e.g., start_for_second_user) and remove the duplicate
implementation start_for_second_user_via_bg, then update any tests that
referenced start_for_second_user_via_bg to call start_for_second_user instead;
alternatively, if distinct behavior was intended, modify
start_for_second_user_via_bg to implement the different scenario (so the
function bodies differ). Ensure you update all references to the removed
function name in tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/aiogram_dialog/manager/bg_manager.py`:
- Around line 263-283: The fg() asynccontextmanager currently creates futures
with asyncio.get_event_loop() and then calls event.as_(bot) which
serializes/reconstructs the event (losing the original event.entered/exited
futures), uses the wrong return type, and can hang if notify_task fails; fix by:
change the return annotation of fg to AsyncIterator[DialogManager]; replace
asyncio.get_event_loop().create_future() with
asyncio.get_running_loop().create_future() for both event.entered and
event.exited; do not call event.as_(bot) (i.e. pass the original DialogFgEvent
instance into DialogUpdate/notify_task so the same futures are used) and
schedule notify_task as a Task, then await completion using
asyncio.wait({event.entered, task}, return_when=FIRST_COMPLETED) so you can
detect if notify_task finished with an exception (raise it) or if event.entered
completed; keep the existing exited.set_result/exited.set_exception handling and
ensure you await the notify task after the context exit.

In `@src/aiogram_dialog/manager/update_handler.py`:
- Around line 31-33: The DialogFgEvent handshake can raise InvalidStateError
because event.entered may be cancelled or already resolved; in the DialogFgEvent
branch inside update_handler (the block that calls
event.entered.set_result(dialog_manager) and awaits event.exited) guard the
future before setting it by checking its state (e.g., only call set_result if
not event.entered.done() and not event.entered.cancelled()), or wrap set_result
in a try/except InvalidStateError and ignore the exception, so replace the
unconditional set_result call with a guarded check or safe try/except to avoid
breaking the update flow.

---

Nitpick comments:
In `@tests/test_fg.py`:
- Around line 33-44: Two test handlers are identical: start_for_second_user and
start_for_second_user_via_bg; consolidate by keeping a single implementation
(e.g., start_for_second_user) and remove the duplicate implementation
start_for_second_user_via_bg, then update any tests that referenced
start_for_second_user_via_bg to call start_for_second_user instead;
alternatively, if distinct behavior was intended, modify
start_for_second_user_via_bg to implement the different scenario (so the
function bodies differ). Ensure you update all references to the removed
function name in tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85eb56f7-526b-4149-b110-f50fb1ca6f64

📥 Commits

Reviewing files that changed from the base of the PR and between f6fb773 and 52e0943.

📒 Files selected for processing (10)
  • example/loading.py
  • src/aiogram_dialog/api/entities/update_event.py
  • src/aiogram_dialog/api/protocols/manager.py
  • src/aiogram_dialog/manager/bg_manager.py
  • src/aiogram_dialog/manager/manager.py
  • src/aiogram_dialog/manager/sub_manager.py
  • src/aiogram_dialog/manager/update_handler.py
  • src/aiogram_dialog/manager/updater.py
  • src/aiogram_dialog/tools/preview.py
  • tests/test_fg.py

Comment on lines +263 to +283
@asynccontextmanager
async def fg(self) -> Iterator[DialogManager]:
event = DialogFgEvent(
data=None,
action=DialogAction.FG,
entered=asyncio.get_event_loop().create_future(),
exited=asyncio.get_event_loop().create_future(),
**self._base_event_params(),
)
bot = self._event_context.bot
update = DialogUpdate(aiogd_update=event.as_(bot)).as_(bot)
task = self._updater.notify_task(bot=bot, update=update)
manager = await event.entered
try:
yield manager
except Exception as e:
event.exited.set_exception(e)
raise e
else:
event.exited.set_result(None)
await task

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how DialogFgEvent and DialogUpdate handle as_() and whether Future fields are preserved

echo "=== Check DialogFgEvent definition ==="
ast-grep --pattern $'class DialogFgEvent($_):
  $$$
'

echo "=== Check DialogUpdate definition ==="
ast-grep --pattern $'class DialogUpdate($_):
  $$$
'

echo "=== Check if as_() is overridden in these classes ==="
rg -n "def as_\(" --type py -C 3

Repository: Tishka17/aiogram_dialog

Length of output: 1450


🌐 Web query:

Python asyncio.get_event_loop deprecation asyncio.get_running_loop

💡 Result:

asyncio.get_event_loop is deprecated in scenarios where no current event loop is set, with changes escalating across Python versions. In Python 3.10-3.11, it emitted DeprecationWarning when no running loop existed. Since Python 3.12, it warns when no running or current loop is set. In Python 3.14 (current as of 2026-03-21), it raises RuntimeError if no current event loop exists. The asyncio policy system, which supported implicit loop creation, is deprecated since 3.14 and slated for removal in 3.16. The recommended replacement is asyncio.get_running_loop, which returns the running event loop in the current thread but raises RuntimeError if no loop is running. It can only be called from coroutines or callbacks and has simpler, more predictable behavior. Key differences: - get_event_loop: Complex logic; falls back to policy if no running loop; now errors without set loop. - get_running_loop: Strictly returns running loop or errors; preferred in async contexts. For code needing a loop outside running contexts (e.g., synchronous code), use asyncio.new_event_loop explicitly, set it with asyncio.set_event_loop, and manage its lifecycle (run_until_complete, shutdown, close). Prefer high-level asyncio.run for top-level entry points. Example migration in async context: # Old (deprecated) loop = asyncio.get_event_loop # New loop = asyncio.get_running_loop Outside async: # Old (now errors in 3.14) loop = asyncio.get_event_loop # New loop = asyncio.new_event_loop asyncio.set_event_loop(loop) try: loop.run_until_complete(coro) finally: loop.run_until_complete(loop.shutdown_asyncgens) loop.close Or better, use asyncio.run(coro).

Citations:


🏁 Script executed:

#!/bin/bash

# Check parent class of DialogUpdateEvent
echo "=== DialogUpdateEvent parent class ==="
rg -n "class DialogUpdateEvent" -A 1 --type py

# Check Update class and as_() implementation
echo "=== Update class from aiogram ==="
rg -n "class Update" -A 5 --type py | head -50

# Check if DialogFgEvent or DialogUpdateEvent override as_()
echo "=== DialogFgEvent and DialogUpdateEvent implementations ==="
rg -n "class DialogFgEvent\|class DialogUpdateEvent" -A 20 --type py src/aiogram_dialog/api/entities/update_event.py

# Check the fg() method return type annotation
echo "=== fg() method definition ==="
rg -n "async def fg" -B 1 -A 2 --type py src/aiogram_dialog/manager/bg_manager.py

Repository: Tishka17/aiogram_dialog

Length of output: 1019


🏁 Script executed:

#!/bin/bash

# Search for as_() method implementation in TelegramObject or base classes
echo "=== Search for as_() method implementations ==="
rg -n "def as_\(" -B 2 -A 10 --type py | head -100

# Check if DialogUpdateEvent or DialogFgEvent has field exclusion or special handling
echo "=== Check for field handling in update_event.py ==="
cat src/aiogram_dialog/api/entities/update_event.py | head -100

Repository: Tishka17/aiogram_dialog

Length of output: 1657


🏁 Script executed:

#!/bin/bash

# Check for TelegramObject import and search in dependencies
echo "=== Check if TelegramObject is from aiogram ==="
rg -n "from aiogram" src/aiogram_dialog/api/entities/update_event.py

# Look for handle_update implementation in bg_manager.py
echo "=== Search for handle_update in bg_manager ==="
rg -n "def handle_update\|event\.entered\|event\.exited" --type py src/aiogram_dialog/manager/bg_manager.py

# Search for how the event is processed in the update handler
echo "=== Search for DialogFgEvent handling ==="
rg -n "DialogFgEvent" --type py -B 2 -A 5

# Check the notify_task method to understand the flow
echo "=== Search for notify_task ==="
rg -n "def notify_task\|notify_task" --type py src/aiogram_dialog/manager/bg_manager.py -A 10 | head -60

Repository: Tishka17/aiogram_dialog

Length of output: 3365


🏁 Script executed:

#!/bin/bash

# Check project's Python version requirements
echo "=== Check Python version requirements ==="
fd -e "pyproject.toml" -o -e "setup.py" -o -e "setup.cfg" | xargs cat 2>/dev/null | grep -i "python" | head -20

# Check if get_running_loop is already used anywhere in the project
echo "=== Check for get_running_loop usage ==="
rg -n "get_running_loop" --type py | head -20

Repository: Tishka17/aiogram_dialog

Length of output: 403


🏁 Script executed:

#!/bin/bash

# Check if TelegramObject.as_() serializes to dict
echo "=== Check TelegramObject import and understand model behavior ==="
rg -n "TelegramObject\|model_dump\|model_validate" src/aiogram_dialog/api/entities/update_event.py

# Check the actual flow - how is the event passed through the system
echo "=== Check how update is created and passed ==="
sed -n '270,280p' src/aiogram_dialog/manager/bg_manager.py

# Check notify_task signature
echo "=== Check notify_task method signature ==="
rg -n "def notify_task" src/aiogram_dialog/manager/updater.py -A 5

# Check if the event object is reused or reconstructed
echo "=== Check if event is reconstructed from update ==="
rg -n "aiogd_update\|event =" src/aiogram_dialog/manager/update_handler.py -B 2 -A 2 | head -40

Repository: Tishka17/aiogram_dialog

Length of output: 966


Critical: Future references are lost during event serialization, causing indefinite hang.

When event.as_(bot) is called on line 273, the event is serialized and reconstructed. The DialogFgEvent object passed to notify_task is no longer the same instance with the live entered and exited Future objects created on lines 268-269. When the update handler receives this reconstructed event and attempts event.entered.set_result(dialog_manager) (update_handler.py:32), it sets the result on a different Future instance than the one being awaited on line 276, causing an indefinite hang.

Additionally:

  1. Type annotation: Return type should be AsyncIterator[DialogManager], not Iterator[DialogManager] (line 264).
  2. Deprecation: asyncio.get_event_loop() is deprecated in Python 3.10+ and raises RuntimeError in Python 3.14+. Use asyncio.get_running_loop() instead (lines 268-269).
  3. Error handling: If notify_task fails or is cancelled before event.entered is set, await event.entered will hang indefinitely with no timeout or cancellation mechanism.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/aiogram_dialog/manager/bg_manager.py` around lines 263 - 283, The fg()
asynccontextmanager currently creates futures with asyncio.get_event_loop() and
then calls event.as_(bot) which serializes/reconstructs the event (losing the
original event.entered/exited futures), uses the wrong return type, and can hang
if notify_task fails; fix by: change the return annotation of fg to
AsyncIterator[DialogManager]; replace asyncio.get_event_loop().create_future()
with asyncio.get_running_loop().create_future() for both event.entered and
event.exited; do not call event.as_(bot) (i.e. pass the original DialogFgEvent
instance into DialogUpdate/notify_task so the same futures are used) and
schedule notify_task as a Task, then await completion using
asyncio.wait({event.entered, task}, return_when=FIRST_COMPLETED) so you can
detect if notify_task finished with an exception (raise it) or if event.entered
completed; keep the existing exited.set_result/exited.set_exception handling and
ensure you await the notify task after the context exit.

Comment on lines +31 to +33
elif isinstance(event, DialogFgEvent):
event.entered.set_result(dialog_manager)
await event.exited

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard foreground handshake futures before setting results.

At Line 32, set_result() is unconditional. If event.entered was cancelled by the waiting task, this can raise InvalidStateError and break the update flow.

💡 Proposed fix
     elif isinstance(event, DialogFgEvent):
-        event.entered.set_result(dialog_manager)
+        if event.entered.cancelled():
+            return
+        if not event.entered.done():
+            event.entered.set_result(dialog_manager)
         await event.exited
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/aiogram_dialog/manager/update_handler.py` around lines 31 - 33, The
DialogFgEvent handshake can raise InvalidStateError because event.entered may be
cancelled or already resolved; in the DialogFgEvent branch inside update_handler
(the block that calls event.entered.set_result(dialog_manager) and awaits
event.exited) guard the future before setting it by checking its state (e.g.,
only call set_result if not event.entered.done() and not
event.entered.cancelled()), or wrap set_result in a try/except InvalidStateError
and ignore the exception, so replace the unconditional set_result call with a
guarded check or safe try/except to avoid breaking the update flow.

@Tishka17
Tishka17 merged commit bb05af1 into develop Mar 21, 2026
7 of 12 checks passed
@Tishka17
Tishka17 deleted the feature/fg branch March 21, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant